import { Draw } from "../Draw/Draw";

type HE = HTMLElement;

/**
 * 虚拟滚动以实现平移缩放功能
 */
export class VirtualScroll {
  private draw: Draw;
  private rootBox: Element;
  private zoomBoxSelector: string;
  private konvaBoxSelector: string;

  constructor(draw: Draw) {
    this.draw = draw;
    const eventbus = this.draw.getEventBus();
    this.rootBox = this.draw.getRootBox();
    this.zoomBoxSelector = ".konva-root-container-zoom";
    this.konvaBoxSelector = ".konva-root-container-konvabox";

    this.initScroll();

    eventbus.on("zoom", this.initScroll.bind(this));
    eventbus.on("translate", this.translateHandle.bind(this));
  }

  /**
   * 初始化滚动事件
   *  此事件仅负责监听 konva-root-container-zoom 宽高变化，
   *  判断是否需要显示滚动条
   */
  private initScroll() {
    // 获取 konva-root-container-zoomBox 的宽高 这个是可视区尺寸
    const zoomBox = <HE>this.rootBox.querySelector(this.zoomBoxSelector);
    const zoomBoxWidth = zoomBox.clientWidth;
    const zoomBoxHeight = zoomBox.clientHeight;

    // 获取 konva-root-container-konvabox 的宽高
    const konvaBox = <HE>this.rootBox.querySelector(this.konvaBoxSelector);
    const konvaBoxWidth = konvaBox.clientWidth;
    const konvaBoxHeight = konvaBox.clientHeight;

    // 获取当前的缩放比例
    const scale = this.getKonvaBoxScale(konvaBox);

    // 是否显示滚动条
    const showScrollX = konvaBoxWidth * scale > zoomBoxWidth;
    const showScrollY = konvaBoxHeight * scale > zoomBoxHeight;

    // 判断当前状态下，滚动条是否存在
    let scrollBarX = <HE>zoomBox.querySelector(".scroll-x");
    let scrollBarY = <HE>zoomBox.querySelector(".scroll-y");

    // 此时，滚动条的宽高
    const scrollBarXWidth = zoomBoxWidth - (zoomBoxWidth * scale) / 4;
    const scrollBarHeight = zoomBoxHeight - (zoomBoxHeight * scale) / 4;

    /**
     * 公共事件
     */
    const handleScrollX = (e: MouseEvent) => this.handleScroll(e, "x");
    const handleScrollY = (e: MouseEvent) => this.handleScroll(e, "y");

    // 处理 X
    if (showScrollX && !scrollBarX) {
      scrollBarX = document.createElement("div");
      scrollBarX.className = "scroll-x";
      scrollBarX.style.width = scrollBarXWidth + "px";
      zoomBox.appendChild(scrollBarX);
      this.setScrollCenter(scrollBarX);
      scrollBarX.addEventListener("mousedown", handleScrollX.bind(this));
    } else if (showScrollX && scrollBarX) {
      scrollBarX.style.width = scrollBarXWidth + "px";
      this.setScrollCenter(scrollBarX);
    } else {
      scrollBarX?.remove();
      scrollBarX?.removeEventListener("mousedown", handleScrollX.bind(this));
    }

    // 处理 Y
    if (showScrollY && !scrollBarY) {
      scrollBarY = document.createElement("div");
      scrollBarY.className = "scroll-y";
      scrollBarY.style.height = scrollBarHeight + "px";
      zoomBox.appendChild(scrollBarY);
      this.setScrollCenter(scrollBarY);
      scrollBarY.addEventListener("mousedown", handleScrollY.bind(this));
    } else if (showScrollY && scrollBarY) {
      scrollBarY.style.height = scrollBarHeight + "px";
      this.setScrollCenter(scrollBarY);
    } else {
      scrollBarY?.remove();
      scrollBarY?.removeEventListener("mousedown", handleScrollY.bind(this));
    }
  }

  /**
   * 将滚动公共事件提取出来，传入方向即可 此事件为 mousedown 触发
   * @param e
   * @param direction
   */
  private handleScroll(e: MouseEvent, direction: "x" | "y") {
    // 获取 pageX pageY ,记录初始状态
    const startPageX = e.pageX;
    const startPageY = e.pageY;

    // 获取 konva-root-container-zoomBox 的宽高 这个是可视区尺寸
    const zoomBox = <HE>this.rootBox.querySelector(this.zoomBoxSelector);
    const zoomBoxWidth = zoomBox.clientWidth;
    const zoomBoxHeight = zoomBox.clientHeight;

    // 获取 konva-root-container-konvabox 的宽高
    const konvaBox = <HE>this.rootBox.querySelector(this.konvaBoxSelector);
    // const konvaBoxWidth = konvaBox.clientWidth;
    // const konvaBoxHeight = konvaBox.clientHeight;

    // 获取滚动条
    const scrollBarX = <HE>zoomBox.querySelector(".scroll-x");
    const scrollBarY = <HE>zoomBox.querySelector(".scroll-y");

    // 当前 konvaBox 的缩放比例是会变的
    const scale = this.getKonvaBoxScale(konvaBox);

    // 重新计算当前的滚动条宽度、高度
    const scrollBarXWidth = zoomBoxWidth - (zoomBoxWidth * scale) / 4;
    const scrollBarHeight = zoomBoxHeight - (zoomBoxHeight * scale) / 4;

    // 滚动条可移动范围
    const scrollRangeX = (zoomBoxWidth - scrollBarXWidth) / 2;
    const scrollRangeY = (zoomBoxHeight - scrollBarHeight) / 2;

    // konvaBox 可移动范围
    // const konvaBoxRangeX = (konvaBoxWidth * scale) / 2 - zoomBoxWidth / 2;
    // const konvaBoxRangeY = (konvaBoxHeight * scale) / 2 - zoomBoxHeight / 2;

    // 记录 scrollBarX 初始偏移量 保留 calc 格式
    const startTranslateX = this.parseScrollBarTranslate(scrollBarX, "calc");
    const startTranslateY = this.parseScrollBarTranslate(scrollBarY, "calc");

    // 记录 konvaBox 初始偏移量
    const { stx, sty } = this.parseKonvaBoxTranslate(konvaBox);

    // 使用 document 进行移动
    const mousemove = (e: MouseEvent) => {
      // 计算差值
      const diffX = e.pageX - startPageX;
      const diffY = e.pageY - startPageY;

      // 解析当前的滚动条 translateX、translateY 的值
      if (direction === "x") {
        // 当前的 translateX 的值
        const ctx = <number>this.parseScrollBarTranslate(scrollBarX);

        let scroll = `calc(${diffX}px + ${startTranslateX})`;
        const konva = `scale(${scale}) translateX(${-diffX}px)  translateY(${stx}px)`; // 移动的同时需要加上缩放

        // 向右滚动
        if (diffX > 0 && ctx >= scrollRangeX) {
          scroll = `calc(-50% + ${scrollRangeX}px)`;
        }
        if (diffX < 0 && ctx <= -scrollRangeX) {
          scroll = `calc(-50% - ${scrollRangeX}px)`;
        }

        this.updateTranslate("x", { scroll, konva });
      }

      if (direction === "y") {
        const cty = <number>this.parseScrollBarTranslate(scrollBarY);
        let scroll = `calc(${diffY}px + ${startTranslateY})`;
        const konva = `scale(${scale}) translateX(${sty}px)  translateY(${-diffY}px)`; // 移动的同时需要加上缩放

        // 向上滚动
        if (diffY > 0 && cty >= scrollRangeY) {
          scroll = `calc(-50% + ${scrollRangeY}px)`;
        }
        if (diffY < 0 && cty <= -scrollRangeY) {
          scroll = `calc(-50% - ${scrollRangeY}px)`;
        }

        this.updateTranslate("y", { scroll, konva });
      }
    };

    const mouseup = () => {
      document.removeEventListener("mousemove", mousemove);
      document.removeEventListener("mouseup", mouseup);
    };
    document.addEventListener("mousemove", mousemove);
    document.addEventListener("mouseup", mouseup);

    e.stopPropagation();
  }

  /**
   * 平移事件实现
   * @param payload
   */
  private translateHandle(payload: {
    dx: number;
    dy: number;
    baseStyle: string;
  }) {
    const konvaBox = <HE>(
      this.rootBox.querySelector(".konva-root-container-konvabox")
    );
    // 此处的 dx dy 是画布的偏移量，还需要计算出 scroll 同步的距离
    const { dx, dy, baseStyle } = payload;
    // 这里需要同时处理 水平垂直的滚动条加 konvaBox 的平移
    // 获取当前 konvaBox 的缩放比例
    const scale = this.getKonvaBoxScale(konvaBox);

    const style = baseStyle || "scale(1)";

    // 获取 konvaBox 的偏移量
    const { stx, sty } = this.parseKonvaBoxTranslate(style);

    // 偏移量直接 + dx dy
    const transform = `scale(${scale}) translateX(${stx + dx}px) translateY( ${
      sty + dy
    }px)`;

    konvaBox.style.transform = transform;
  }

  /**
   * 为了实现 平移、缩放 公用事件，应该将实际的 修改 translate 的函数抽离出来
   * 传入移动方向，移动距离即可
   * @param direction x | y
   * @param distance 移动距离 {scroll: number, konva: number}
   *
   */
  private updateTranslate(
    direction: "x" | "y" | "all",
    distance: { scroll: string; konva: string }
  ) {
    const zoomBox = <HE>this.rootBox.querySelector(this.zoomBoxSelector);
    const konvaBox = <HE>this.rootBox.querySelector(this.konvaBoxSelector);

    if (direction === "x") {
      // 移动 scroll
      const scrollBarX = <HE>zoomBox.querySelector(".scroll-x");
      scrollBarX.style.transform = `translateX(${distance.scroll})`;
      // 移动 konvaBox
      konvaBox.style.transform = distance.konva;
    }

    if (direction === "y") {
      // 移动 scroll
      const scrollBarY = <HE>zoomBox.querySelector(".scroll-y");
      scrollBarY.style.transform = `translateY(${distance.scroll})`;
      // 移动 konvaBox
      konvaBox.style.transform = distance.konva;
    }
  }

  /**
   * 辅助函数 - 获取当前缩放比
   * @param konvaBox
   * @returns { Number } scale
   */
  private getKonvaBoxScale(konvaBox: HE) {
    const scaleRegex = /scale\((\d+(\.\d+)?)\)/;
    const match = konvaBox.style.transform.match(scaleRegex);
    if (match && match[1]) return parseFloat(match[1]);
    else return 1;
  }

  /**
   * 辅助函数 - 始终保持滚动条居中
   * @param target
   */
  private setScrollCenter(target: HE) {
    // 判断 target 的 classList
    if (target.classList.contains("scroll-x")) {
      target.style.transform = `translateX(-50%)`;
      target.style.left = "50%";
    } else if (target.classList.contains("scroll-y")) {
      target.style.transform = `translateY(-50%)`;
      target.style.top = "50%";
    }
  }

  /**
   * 辅助函数 - 解析 scrollBar 的translateX、translateY
   */
  private parseScrollBarTranslate(scrollBar: HE, format?: "number" | "calc") {
    if (!scrollBar) return;

    // 匹配正则
    let regex: RegExp | null = null;
    if (scrollBar.classList.contains("scroll-x")) {
      regex = /translateX\((.+)\)/;
    } else if (scrollBar.classList.contains("scroll-y")) {
      regex = /translateY\((.+)\)/;
    }
    if (!regex) return 0;

    const match = scrollBar.style.transform.match(regex);

    if (match && match[1] && format === "calc") {
      // 如果保留 calc 的结构 则直接返回 即可
      return match[1].replace(/calc|\(|\)/g, "");
    }

    if (match && match[1]) {
      const arr = match[1].replace(/calc|\(|\)|-50%|px/g, "").split(" ");
      if (arr && arr.length > 1) return Number(arr[1] + arr[2]);
      else return 0;
    } else {
      return 0;
    }
  }

  /**
   * 辅助函数 - 解析 konvaBox 的 translateX、translateY 这里有 scale、calc()  需要进行处理
   */
  private parseKonvaBoxTranslate(konvaBox: HE | string) {
    let stx = 0;
    let sty = 0;

    const regexX = /translateX\((.+)\)/;
    const regexY = /translateY\((.+)\)/;

    const transform =
      typeof konvaBox === "string" ? konvaBox : konvaBox.style.transform;

    const matchX = transform.match(regexX);
    if (matchX && matchX[1]) {
      stx = Number(matchX[1].split(")")[0].replace(/px/g, ""));
    }

    const matchY = transform.match(regexY);
    if (matchY && matchY[1]) {
      sty = Number(matchY[1].split(")")[0].replace(/px/g, ""));
    }

    return { stx, sty };
  }
}
